/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.http.client;

import java.util.HashMap;
import java.util.Map;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import com.google.gwt.xhr.client.XMLHttpRequest;


/**
 * Builder for constructing {@link com.google.gwt.http.client.Request} objects.
 * 
 * <h3>Required Module</h3> Modules that use this class should inherit
 * <code>com.google.gwt.http.HTTP</code>.
 * 
 * {@gwt.include
 * com/google/gwt/examples/http/InheritsExample.gwt.xml}
 * 
 */
public class RequestBuilder {
    /**
     * HTTP request method constants.
     */
    public static final class Method {
        private final String name;

        private Method(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return name;
        }
    }

    /**
     * Specifies that the HTTP DELETE method should be used.
     */
    public static final Method DELETE = new Method("DELETE");

    /**
     * Specifies that the HTTP GET method should be used.
     */
    public static final Method GET = new Method("GET");

    /**
     * Specifies that the HTTP HEAD method should be used.
     */
    public static final Method HEAD = new Method("HEAD");

    /**
     * Specifies that the HTTP POST method should be used.
     */
    public static final Method POST = new Method("POST");

    /**
     * Specifies that the HTTP PUT method should be used.
     */
    public static final Method PUT = new Method("PUT");

    /**
     * The callback to call when the request completes.
     */
    private RequestCallback callback;

    /**
     * Map of header name to value that will be added to the JavaScript
     * XmlHttpRequest object before sending a request.
     */
    private Map<String, String> headers;

    /**
     * HTTP method to use when opening a JavaScript XmlHttpRequest object.
     */
    private final String httpMethod;

    /**
     * Password to use when opening a JavaScript XmlHttpRequest object.
     */
    private String password;

    /**
     * Request data to use when sending a JavaScript XmlHttpRequest object.
     */
    private String requestData;

    /**
     * Timeout in milliseconds before the request timeouts and fails.
     */
    private int timeoutMillis;

    /**
     * URL to use when opening a JavaScript XmlHttpRequest object.
     */
    private final String url;

    /**
     * User to use when opening a JavaScript XmlHttpRequest object.
     */
    private String user;

    /**
     * Creates a builder using the parameters for configuration.
     * 
     * @param httpMethod HTTP method to use for the request
     * @param url URL that has already has already been encoded. Please see
     *          {@link com.google.gwt.http.client.URL#encode(String)} and
     *          {@link com.google.gwt.http.client.URL#encodeComponent(String)} for
     *          how to do this.
     * @throws IllegalArgumentException if the httpMethod or URL are empty
     * @throws NullPointerException if the httpMethod or the URL are null
     */
    public RequestBuilder(Method httpMethod, String url) {
        this((httpMethod == null) ? null : httpMethod.toString(), url);
    }

    /**
     * Creates a builder using the parameters values for configuration.
     * 
     * @param httpMethod HTTP method to use for the request
     * @param url URL that has already has already been URL encoded. Please see
     *          {@link com.google.gwt.http.client.URL#encode(String)} and
     *          {@link com.google.gwt.http.client.URL#encodeComponent(String)} for
     *          how to do this.
     * @throws IllegalArgumentException if the httpMethod or URL are empty
     * @throws NullPointerException if the httpMethod or the URL are null
     */
    protected RequestBuilder(String httpMethod, String url) {

        StringValidator.throwIfEmptyOrNull("httpMethod", httpMethod);
        StringValidator.throwIfEmptyOrNull("url", url);

        this.httpMethod = httpMethod;
        this.url = url;
    }

    /**
     * Returns the callback previously set by
     * {@link #setCallback(RequestCallback)}, or <code>null</code> if no callback
     * was set.
     */
    public RequestCallback getCallback() {
        return callback;
    }

    /**
     * Returns the value of a header previous set by
     * {@link #setHeader(String, String)}, or <code>null</code> if no such header
     * was set.
     * 
     * @param header the name of the header
     */
    public String getHeader(String header) {
        if (headers == null) {
            return null;
        }
        return headers.get(header);
    }

    /**
     * Returns the HTTP method specified in the constructor.
     */
    public String getHTTPMethod() {
        return httpMethod;
    }

    /**
     * Returns the password previously set by {@link #setPassword(String)}, or
     * <code>null</code> if no password was set.
     */
    public String getPassword() {
        return password;
    }

    /**
     * Returns the requestData previously set by {@link #setRequestData(String)},
     * or <code>null</code> if no requestData was set.
     */
    public String getRequestData() {
        return requestData;
    }

    /**
     * Returns the timeoutMillis previously set by {@link #setTimeoutMillis(int)},
     * or <code>0</code> if no timeoutMillis was set.
     */
    public int getTimeoutMillis() {
        return timeoutMillis;
    }

    /**
     * Returns the HTTP URL specified in the constructor.
     */
    public String getUrl() {
        return url;
    }

    /**
     * Returns the user previously set by {@link #setUser(String)}, or
     * <code>null</code> if no user was set.
     */
    public String getUser() {
        return user;
    }

    /**
     * Sends an HTTP request based on the current builder configuration. If no
     * request headers have been set, the header "Content-Type" will be used with
     * a value of "text/plain; charset=utf-8". You must call
     * {@link #setRequestData(String)} and {@link #setCallback(RequestCallback)}
     * before calling this method.
     * 
     * @return a {@link Request} object that can be used to track the request
     * @throws RequestException if the call fails to initiate
     * @throws NullPointerException if a request callback has not been set
     */
    public Request send() throws RequestException {
        StringValidator.throwIfNull("callback", callback);
        return doSend(requestData, callback);
    }

    /**
     * Sends an HTTP request based on the current builder configuration with the
     * specified data and callback. If no request headers have been set, the
     * header "Content-Type" will be used with a value of "text/plain;
     * charset=utf-8". This method does not cache <code>requestData</code> or
     * <code>callback</code>.
     * 
     * @param requestData the data to send as part of the request
     * @param callback the response handler to be notified when the request fails
     *          or completes
     * @return a {@link Request} object that can be used to track the request
     * @throws NullPointerException if <code>callback</code> <code>null</code>
     */
    public Request sendRequest(String requestData, RequestCallback callback) 
        throws RequestException {
        StringValidator.throwIfNull("callback", callback);
        return doSend(requestData, callback);
    }

    /**
     * Sets the response handler for this request. This method <b>must</b> be
     * called before calling {@link #send()}.
     * 
     * @param callback the response handler to be notified when the request fails
     *          or completes
     * 
     * @throws NullPointerException if <code>callback</code> is <code>null</code>
     */
    public void setCallback(RequestCallback callback) {
        StringValidator.throwIfNull("callback", callback);

        this.callback = callback;
    }

    /**
     * Sets a request header with the given name and value. If a header with the
     * specified name has already been set then the new value overwrites the
     * current value.
     * 
     * @param header the name of the header
     * @param value the value of the header
     * 
     * @throws NullPointerException if header or value are null
     * @throws IllegalArgumentException if header or value are the empty string
     */
    public void setHeader(String header, String value) {
        StringValidator.throwIfEmptyOrNull("header", header);
        StringValidator.throwIfEmptyOrNull("value", value);

        if (headers == null) {
            headers = new HashMap<String, String>();
        }

        headers.put(header, value);
    }

    /**
     * Sets the password to use in the request URL. This is ignored if there is no
     * user specified.
     * 
     * @param password password to use in the request URL
     * 
     * @throws IllegalArgumentException if the password is empty
     * @throws NullPointerException if the password is null
     */
    public void setPassword(String password) {
        StringValidator.throwIfEmptyOrNull("password", password);

        this.password = password;
    }

    /**
     * Sets the data to send as part of this request. This method <b>must</b> be
     * called before calling {@link #send()}.
     * 
     * @param requestData the data to send as part of the request
     */
    public void setRequestData(String requestData) {
        this.requestData = requestData;
    }

    /**
     * Sets the number of milliseconds to wait for a request to complete. Should
     * the request timeout, the
     * {@link com.google.gwt.http.client.RequestCallback#onError(Request, Throwable)}
     * method will be called on the callback instance given to the
     * {@link com.google.gwt.http.client.RequestBuilder#sendRequest(String, RequestCallback)}
     * method. The callback method will receive an instance of the
     * {@link com.google.gwt.http.client.RequestTimeoutException} class as its
     * {@link java.lang.Throwable} argument.
     * 
     * @param timeoutMillis number of milliseconds to wait before canceling the
     *          request, a value of zero disables timeouts
     * 
     * @throws IllegalArgumentException if the timeout value is negative
     */
    public void setTimeoutMillis(int timeoutMillis) {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("Timeouts cannot be negative");
        }

        this.timeoutMillis = timeoutMillis;
    }

    /**
     * Sets the user name that will be used in the request URL.
     * 
     * @param user user name to use
     * @throws IllegalArgumentException if the user is empty
     * @throws NullPointerException if the user is null
     */
    public void setUser(String user) {
        StringValidator.throwIfEmptyOrNull("user", user);

        this.user = user;
    }
    
    
    /**
     * Returns the RPC context JSON data. 
     * 
     * @return the RPC context data, or null if none was set 
     */
    private native String getRpcContext() /*-{
		if ($wnd.ocmsRpcContext) {
			return JSON.stringify($wnd.ocmsRpcContext);
		}
		return null;
    }-*/;

    /**
     * Sends an HTTP request based on the current builder configuration. If no
     * request headers have been set, the header "Content-Type" will be used with
     * a value of "text/plain; charset=utf-8".
     * 
     * @return a {@link Request} object that can be used to track the request
     * @throws RequestException if the call fails to initiate
     * @throws NullPointerException if request data has not been set
     * @throws NullPointerException if a request callback has not been set
     */
    private Request doSend(String requestData, final RequestCallback callback) 
        throws RequestException {
        XMLHttpRequest xmlHttpRequest = XMLHttpRequest.create();
        evaluateSync(requestData);
        try {
            if ((user != null) && (password != null)) {
                if (m_sync) {
                    xmlHttpRequest.openSync(httpMethod, url, user, password);
                } else {
                    xmlHttpRequest.open(httpMethod, url, user, password);
                }
            } else if (user != null) {
                if (m_sync) {
                    xmlHttpRequest.openSync(httpMethod, url, user);
                } else {
                    xmlHttpRequest.open(httpMethod, url, user);
                }
            } else {
                if (m_sync) {
                    xmlHttpRequest.openSync(httpMethod, url);
                } else {
                    xmlHttpRequest.open(httpMethod, url);
                }
            }
        } catch (JavaScriptException e) {
            RequestPermissionException requestPermissionException = new RequestPermissionException(url);
            requestPermissionException.initCause(new RequestException(e.getMessage()));
            throw requestPermissionException;
        }
        String rpcContext = getRpcContext();
        if (rpcContext != null) {
            setHeader("X-OcmsRpcContext", rpcContext); 
            
        }
        setHeaders(xmlHttpRequest);

        final Request request = new Request(xmlHttpRequest, timeoutMillis, callback);

        if (!m_sync) {
            // Must set the onreadystatechange handler before calling send().
            xmlHttpRequest.setOnReadyStateChange(new ReadyStateChangeHandler() {

                public void onReadyStateChange(XMLHttpRequest xhr) {

                    if (xhr.getReadyState() == XMLHttpRequest.DONE) {
                        xhr.clearOnReadyStateChange();
                        request.fireOnResponseReceived(callback);
                    }
                }
            });
        }

        try {
            xmlHttpRequest.send(requestData);
        } catch (JavaScriptException e) {
            throw new RequestException(e.getMessage());
        }

        if (m_sync) {
            xmlHttpRequest.clearOnReadyStateChange();
            request.fireOnResponseReceived(callback);
        }

        return request;
    }
    
    /**
     * Checks whether the request data contains the sync RPC token.<p>
     * 
     * This method was introduced to allow synchronised RPC calls within vaadin.
     *   
     * @param data the request data
     */
    private void evaluateSync(String data){
        if (!m_sync && data!=null && data.contains(org.opencms.gwt.client.rpc.CmsRpcAction.SYNC_TOKEN)){
            m_sync=true;
        }
    }

    /*
     * Internal method that actually sets our cached headers on the underlying
     * JavaScript XmlHttpRequest object. If there are no headers set, then we set
     * the "Content-Type" to "text/plain; charset=utf-8". This is really lining us
     * up for integration with RPC.
     */
    private void setHeaders(XMLHttpRequest xmlHttpRequest) throws RequestException {

        if ((headers != null) && (headers.size() > 0)) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                try {
                    xmlHttpRequest.setRequestHeader(header.getKey(), header.getValue());
                } catch (JavaScriptException e) {
                    throw new RequestException(e.getMessage());
                }
            }
        } else {
            xmlHttpRequest.setRequestHeader("Content-Type", "text/plain; charset=utf-8");
        }
    }

    private boolean m_sync = false;

    /**
     * Returns the sync.<p>
     *
     * @return the sync
     */
    public boolean isSync() {

        return m_sync;
    }

    /**
     * Sets the sync.<p>
     *
     * @param sync the sync to set
     */
    public void setSync(boolean sync) {

        m_sync = sync;
    }
}
